package com.stars.easyms.datasource.interceptor.mybatis;

import com.stars.easyms.datasource.EasyMsDataSource;
import com.stars.easyms.datasource.EasyMsDataSourceMonitor;
import com.stars.easyms.datasource.EasyMsMasterSlaveDataSource;
import com.stars.easyms.datasource.EasyMsMultiDataSource;
import com.stars.easyms.datasource.common.EasyMsDataSourceConstant;
import com.stars.easyms.datasource.enums.DatabaseType;
import com.stars.easyms.datasource.exception.DataSourceInterceptorException;
import com.stars.easyms.datasource.holder.EasyMsInterceptorSkipHolder;
import com.stars.easyms.datasource.holder.EasyMsMasterSlaveDataSourceHolder;
import com.stars.easyms.datasource.holder.EasyMsMasterSlaveHolder;
import com.stars.easyms.base.util.ExceptionUtil;
import com.stars.easyms.base.util.ThreadUtil;
import com.stars.easyms.datasource.holder.EasyMsSynchronizationManager;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import static com.stars.easyms.datasource.common.SqlExceptionErrorCodeConstant.ORACLE_ERROR_CODE_RETRY;

/**
 * <p>className: BaseEasyMsMybatisInterceptor</p>
 * <p>description: EasyMs的Mybatis基础拦截器</p>
 *
 * @author guoguifang
 * @version 1.2.1
 * @date 2019-05-13 10:55
 */
public abstract class BaseEasyMsMybatisInterceptor implements Interceptor {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final Pattern EXCLUDE_QUERY = Pattern.compile(".+\\w+\\.(currval|nextval)\\s+.+", Pattern.CASE_INSENSITIVE);

    private EasyMsMultiDataSource easyMsMultiDataSource;

    private EasyMsDataSourceMonitor easyMsDataSourceMonitor = EasyMsDataSourceMonitor.getInstance();

    protected BaseEasyMsMybatisInterceptor(EasyMsMultiDataSource easyMsMultiDataSource) {
        this.easyMsMultiDataSource = easyMsMultiDataSource;
    }

    Object determineDataSourceAndExecute(Invocation invocation, boolean isForceMaster)
            throws InvocationTargetException, IllegalAccessException, DataSourceInterceptorException {
        // 判断过滤器是否需要直接跳过
        if (EasyMsInterceptorSkipHolder.isSkip()) {
            return invocation.proceed();
        }

        // 获取执行序列号和执行SqlId
        BoundSql boundSql = null;
        String sqlId = null;
        Object[] args = invocation.getArgs();
        if (args[0] instanceof MappedStatement) {
            MappedStatement ms = (MappedStatement) args[0];
            sqlId = ms.getId();
            Object parameter = args[1];
            int methodArgsLength = 6;
            if (args.length != methodArgsLength) {
                boundSql = ms.getBoundSql(parameter);
            } else {
                boundSql = (BoundSql) args[5];
            }
        }

        boolean isDetermineDatasource = EasyMsSynchronizationManager.getEasyMsDataSource() == null;
        try {

            // 决定具体使用哪个主从数据源
            EasyMsMasterSlaveDataSource easyMsMasterSlaveDataSource = determineMasterSlaveDataSource();

            // 将确定的数据源存放入Holder类（用作底层数据源决定数据源的依据）并开启监控
            if (isDetermineDatasource) {
                // 如果是非查询类型或者不包含的查询语句需要走主库
                boolean isSwitchMaster = isForceMaster || boundSql == null || EXCLUDE_QUERY.matcher(boundSql.getSql()).matches();
                EasyMsDataSource easyMsDataSource = determineDataSource(easyMsMasterSlaveDataSource, isSwitchMaster);
                EasyMsSynchronizationManager.setEasyMsDataSource(easyMsDataSource);
            }

            return proceed(easyMsMasterSlaveDataSource.getDatabaseType(), sqlId, boundSql, invocation, 0);
        } finally {
            // 清除数据源选择
            if (isDetermineDatasource && !EasyMsSynchronizationManager.isSynchronization()) {
                EasyMsSynchronizationManager.clearEasyMsDataSource();
            }
        }
    }

    private Object proceed(DatabaseType databaseType, String sqlId, BoundSql boundSql, Invocation invocation,
                           int retryCount) throws DataSourceInterceptorException {
        // 处理SQL操作，当执行完成后把正在使用次数减去1
        try {
            // 开启监控
            easyMsDataSourceMonitor.start(sqlId, boundSql);

            // 开始执行sql
            return invocation.proceed();
        } catch (InvocationTargetException invocationTargetException) {

            // 获取具体异常对象
            Throwable throwable = invocationTargetException.getTargetException();
            if (throwable instanceof SQLException) {
                SQLException sqlException = (SQLException) throwable;
                handleSqlException(sqlException, databaseType, sqlId, boundSql, invocation, retryCount);
            }

            // 封装一层异常，记录详细错误信息
            throw new DataSourceInterceptorException("SQL with sqlId[{}] fails to execute! Caused by: {}",
                    sqlId, throwable.getMessage(), throwable);
        } catch (Exception t) {

            // 封装一层异常，记录详细错误信息
            throw new DataSourceInterceptorException("SQL with sqlId[{}] fails to execute! Caused by: {}",
                    sqlId, t.getMessage(), t);
        } finally {
            // 监控结束并清除数据源选择
            easyMsDataSourceMonitor.end();
        }
    }

    private void handleSqlException(SQLException sqlException, DatabaseType databaseType, String sqlId, BoundSql boundSql,
                                    Invocation invocation, int retryCount) throws DataSourceInterceptorException {
        if (databaseType == DatabaseType.ORACLE && ORACLE_ERROR_CODE_RETRY.contains(sqlException.getErrorCode())
                && ++retryCount < EasyMsDataSourceConstant.FAIL_RETRY_COUNT) {
            logger.warn("SQL with sqlId[{}] fails to execute, sql exception: {}, now begin to retry({})!",
                    sqlId, ExceptionUtil.getExceptionDesc(sqlException), retryCount);
            ThreadUtil.sleep(200, TimeUnit.MILLISECONDS);
            proceed(databaseType, sqlId, boundSql, invocation, retryCount);
        } else {
            easyMsDataSourceMonitor.sqlException(sqlException);
        }
    }

    private EasyMsMasterSlaveDataSource determineMasterSlaveDataSource() {
        // 获取数据源，先从线程本地变量中取，如果不存在则取默认主从数据源且无需再存放入线程本地变量中因为后续不会用到
        EasyMsMasterSlaveDataSource easyMsMasterSlaveDataSource = EasyMsMasterSlaveDataSourceHolder.getMasterSlaveDataSource();
        if (easyMsMasterSlaveDataSource == null) {
            easyMsMasterSlaveDataSource = easyMsMultiDataSource.getDefaultDataSource();
        }
        return easyMsMasterSlaveDataSource;
    }

    private EasyMsDataSource determineDataSource(EasyMsMasterSlaveDataSource easyMsMasterSlaveDataSource, boolean isSwitchMaster) {
        // 如果使用从数据源，则先获取从数据源，若从数据源为空则使用主数据源
        EasyMsDataSource easyMsDataSource;
        if (isSwitchMaster || EasyMsMasterSlaveHolder.isAlreadySwitchAndUseMaster()) {
            easyMsDataSource = easyMsMasterSlaveDataSource.getMasterDataSource();
        } else {
            easyMsDataSource = easyMsMasterSlaveDataSource.getSlaveDataSource();
            if (easyMsDataSource == null) {
                easyMsDataSource = easyMsMasterSlaveDataSource.getMasterDataSource();
            }
        }
        return easyMsDataSource;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // Intentionally blank
    }
}